﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using Xceed.Silverlight.Utils;

namespace Xceed.Silverlight.ListBox
{
  internal abstract class TransitionAnimation
  {

    #region Constructor

    internal TransitionAnimation(int duration)
    {
      this.Duration = duration;
    }

    #endregion

    internal event EventHandler<TransitionAnimationEventArgs> Completed;

    #region Duration Property

    internal double Duration
    {
      get;
      set;
    }

    #endregion

    #region HasCompleted Porperty

    internal bool HasCompleted
    {
      get;
      private set;
    }

    #endregion

    #region IsRunning Property

    internal bool IsRunning
    {
      get;
      private set;
    }

    #endregion

    #region StartTime Property

    private int StartTime
    {
      get;
      set;
    }

    #endregion

    #region HandledItems Property

    internal List<ListBoxContainer> HandledItems
    {
      get
      {
        return m_handledItems;
      }
    }

    #endregion

    internal void Initialize( IEnumerable<ListBoxContainer> containers, object state )
    {
      if( containers == null )
        throw new ArgumentNullException( "containers" );

      if( m_initialized )
        throw new InvalidOperationException( "An attempt was made to initialize a TransitionAnimation that is already initialized." );

      m_state = state;

      if( m_initialLayoutInformation != null )
      {
        m_initialLayoutInformation.Clear();
        m_initialLayoutInformation = null;
      }

      m_initialLayoutInformation = TransitionAnimation.CreateLayoutInformation( containers, true );

      //Flag each element of the "old" layout as deferred for size update (avoid too many updates).
      foreach( ListBoxContainer element in m_initialLayoutInformation.Keys )
      {
        element.BeginSizeUpdateDeferring();
      }

      this.HasCompleted = false;
      m_initialized = true;
    }

    internal void StartTransitionAnimation( IEnumerable<ListBoxContainer> containers )
    {
      if( containers == null )
        throw new ArgumentNullException( "containers" );

      if( !m_initialized )
        throw new InvalidOperationException( "An attempt was made to start an uninitialized TransitionAnimation." );

      if( this.IsRunning )
        throw new InvalidOperationException( "An attempt was made to start a TransitionAnimation that has already been started." );

      m_handledItems.Clear();

      Dictionary<ListBoxContainer, ElementLayoutInfo> updatedLayoutInfo = TransitionAnimation.CreateLayoutInformation( containers, false );

      //Flag each element of the "new" layout as deferred for size update (avoid too many updates).
      foreach( ListBoxContainer element in updatedLayoutInfo.Keys )
      {
        element.BeginSizeUpdateDeferring();
      }

      Dictionary<ListBoxContainer, LayoutChangeInfo> changedElements = this.CompareLayouts( m_initialLayoutInformation, updatedLayoutInfo );

      this.DetermineLayoutChangesCore( changedElements );

      foreach( KeyValuePair<ListBoxContainer, LayoutChangeInfo> changeInfo in changedElements )
      {
        //Prepare element for TransitionAnimation
        if( this.PrepareContainer( changeInfo.Key, changeInfo.Value ) )
        {
          //Update the element with start time, ensure that as soon as "start" completes, state if correct for all items.
          if( !this.UpdateContainer( 0d, changeInfo.Key ) )
          {
            //If both preparation let pass the item and update did not filter it out, mark the item as managed by TransitionAnimation
            m_handledItems.Add( changeInfo.Key );
          }
        }
      }

      //deFlag each element of the "new" layout so their size can be updated.
      foreach( ListBoxContainer element in updatedLayoutInfo.Keys )
      {
        element.EndSizeUpdateDeferring();
      }

      //deFlag each element of the "old" layout so their size can be updated.
      foreach( ListBoxContainer element in m_initialLayoutInformation.Keys )
      {
        element.EndSizeUpdateDeferring();
      }

      if( m_handledItems.Count > 0 )
      {
        CompositionTarget.Rendering += OnRendering;
        this.IsRunning = true;

        this.StartTime = Environment.TickCount;
      }
      else
      {
        this.OnCompleted();
      }

    }

    internal abstract void AddContainer( ListBoxContainer container );

    private static Dictionary<ListBoxContainer, ElementLayoutInfo> CreateLayoutInformation( IEnumerable<ListBoxContainer> listBoxElements, bool includeActualAnimationInfo )
    {
      Dictionary<ListBoxContainer, ElementLayoutInfo> dictionary = new Dictionary<ListBoxContainer, ElementLayoutInfo>();

      foreach( ListBoxContainer element in listBoxElements )
      {
        ElementLayoutInfo newInfo = new ElementLayoutInfo();

        Point absoluteElementPosition = element.AbsoluteOffset;


        if( includeActualAnimationInfo )
        {
          absoluteElementPosition = element.CurrentOffset;
          newInfo.Height = element.ActualElementHeight;
          newInfo.Width = element.ActualElementWidth;
          newInfo.Opacity = element.Opacity;
          newInfo.Angle = element.Rotation.Angle;
          newInfo.ScaleFactor = element.Scale.ScaleX;
        }
        else
        {
          absoluteElementPosition = element.AbsoluteOffset;
          newInfo.Height = element.AbsoluteElementHeight;
          newInfo.Width = element.AbsoluteElementWidth;
          newInfo.Opacity = element.AbsoluteOpacity;
          newInfo.Angle = element.AbsoluteRotationAngle;
          newInfo.ScaleFactor = element.AbsoluteScaleFactor;
        }

        newInfo.PositionX = absoluteElementPosition.X;
        newInfo.PositionY = absoluteElementPosition.Y;

        newInfo.MaximumHeight = element.AbsoluteMaximumHeight;
        newInfo.MaximumWidth = element.AbsoluteMaximumWidth;

        dictionary.Add( element, newInfo );
      }

      return dictionary;
    }

    private Dictionary<ListBoxContainer, LayoutChangeInfo> CompareLayouts( Dictionary<ListBoxContainer, ElementLayoutInfo> referenceLayout, Dictionary<ListBoxContainer, ElementLayoutInfo> newLayout )
    {
      Dictionary<ListBoxContainer, LayoutChangeInfo> changesDictionary = new Dictionary<ListBoxContainer, LayoutChangeInfo>();

      List<ListBoxContainer> removedElements = new List<ListBoxContainer>( referenceLayout.Keys );
      List<ListBoxContainer> addedElements = new List<ListBoxContainer>( newLayout.Keys );

      foreach( KeyValuePair<ListBoxContainer, ElementLayoutInfo> newLayoutInfo in newLayout )
      {
        ListBoxContainer key = newLayoutInfo.Key;

        ElementLayoutInfo referenceLayoutInfo;

        if( !referenceLayout.TryGetValue( key, out referenceLayoutInfo ) )
        {
          //if the referenceLayout does not contain the element, its because items was added.
          removedElements.Remove( key );
          continue;
        }

        //If the newLayout's element is present also in referenceLayout, then its not added nor removed.
        addedElements.Remove( key );
        removedElements.Remove( key );

        //perform comparison
        LayoutChangeInfo comparison = this.CompareLayoutInfos( referenceLayoutInfo, newLayoutInfo.Value );

        //If comparison found differences
        if( comparison != null )
        {
          //mark the changes as such
          comparison.ChangeType = ChangeType.ElementChanged;

          //add to dictionary
          changesDictionary.Add( key, comparison );
        }
      }

      //after changes were detected, make sure to add all elements which were removed
      foreach( ListBoxContainer removedElement in removedElements )
      {
        //do not create the LayoutChangeInfo directly, allow factory method to do so, to allow derived class customization.
        LayoutChangeInfo layoutChangeInfo = this.CreateLayoutChangeInfo();

        layoutChangeInfo.ChangeType = ChangeType.ElementRemoved;
        layoutChangeInfo.OpacityChange = -removedElement.Opacity;

        changesDictionary.Add( removedElement, layoutChangeInfo );
      }

      //after removed items were added, make sure to add all elements which were added
      foreach( ListBoxContainer addedElement in addedElements )
      {
        //do not create the LayoutChangeInfo directly, allow factory method to do so, to allow derived class customization.
        LayoutChangeInfo layoutChangeInfo = this.CreateLayoutChangeInfo();

        layoutChangeInfo.OpacityChange = (addedElement.AbsoluteOpacity);
        layoutChangeInfo.ChangeType = ChangeType.ElementAdded;

        changesDictionary.Add( addedElement, layoutChangeInfo );
      }

      return changesDictionary;
    }

    private LayoutChangeInfo CompareLayoutInfos( ElementLayoutInfo referenceLayoutInfo, ElementLayoutInfo newLayoutInfo )
    {
      //do not create the LayoutChangeInfo directly, allow factory method to do so, to allow derived class customization.
      LayoutChangeInfo changeInfo = this.CreateLayoutChangeInfo();

      if( !LayoutUtils.AreClose( referenceLayoutInfo.PositionX, newLayoutInfo.PositionX ) )
      {
        changeInfo.XChange = referenceLayoutInfo.PositionX - newLayoutInfo.PositionX;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.PositionY, newLayoutInfo.PositionY ) )
      {
        changeInfo.YChange = referenceLayoutInfo.PositionY - newLayoutInfo.PositionY;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.Width, newLayoutInfo.Width ) )
      {
        changeInfo.WidthChange = referenceLayoutInfo.Width - newLayoutInfo.Width;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.Height, newLayoutInfo.Height ) )
      {
        changeInfo.HeightChange = referenceLayoutInfo.Height - newLayoutInfo.Height;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.MaximumWidth, newLayoutInfo.MaximumWidth ) )
      {
        changeInfo.MaximumWidthChange = referenceLayoutInfo.MaximumWidth - newLayoutInfo.MaximumWidth;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.MaximumHeight, newLayoutInfo.MaximumHeight ) )
      {
        changeInfo.MaximumHeightChange = referenceLayoutInfo.MaximumHeight - newLayoutInfo.MaximumHeight;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.Opacity, newLayoutInfo.Opacity ) )
      {
        changeInfo.OpacityChange = referenceLayoutInfo.Opacity - newLayoutInfo.Opacity;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.Angle, newLayoutInfo.Angle ) )
      {
        changeInfo.AngleChange = referenceLayoutInfo.Angle - newLayoutInfo.Angle;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.ScaleFactor, newLayoutInfo.ScaleFactor ) )
      {
        changeInfo.ScaleChange = referenceLayoutInfo.ScaleFactor - newLayoutInfo.ScaleFactor;
      }


      if( changeInfo.IsEmpty )
        return null;

      return changeInfo;
    }

    //This method is there to allow derived TransitionAnimation to subclass and add information to the LayoutChangeInfo class
    //to tailor it to their needs. Standard implementation covers basic translations and resizing scenarios
    internal virtual LayoutChangeInfo CreateLayoutChangeInfo()
    {
      return LayoutChangeInfo.CreateDefaultLayoutChangeInfo();
    }

    //This method provides the default implementation for the calculation of position or size for the 
    //LayoutChangeInfo class. While it is no way mandatory for TransitionAnimations to use this method, it is there
    //for convenience purpose.
    internal static double ComputeLayoutInfoValue( double normalizedElapsedTime, double requiredChange )
    {
      return requiredChange + ( normalizedElapsedTime * -requiredChange );
    }

    //This method, called within the DetermineLayoutChanges allows derived class to see the big picture before deciding how
    //to behave with individual list box elements.
    internal abstract void DetermineLayoutChangesCore( Dictionary<ListBoxContainer, LayoutChangeInfo> changedElements );

    //This method, called within the PrepareContainer method, automatically invoked on each "changed" container detected
    //during the StartTransitionAnimationMethod.
    internal abstract bool PrepareContainerCore( ListBoxContainer container, LayoutChangeInfo changeInfo );

    //This method, called within the OnRendering method, allows derived classes to update the LayoutChangeInfo associated with ListBoxContainer and
    //also allow for "custom" animations and actions to be performed on the ListBoxContainer.
    internal abstract bool UpdateContainerCore( double normalizedElapsedTime, ListBoxContainer container );

    //This method, called within the CleanContainer method, allows derived classes to perform any relevant cleanup operation associated with
    //a ListBoxContainer animation. It is there to "compensate" for any action undertaken in the prepare and update abstract methods.
    internal abstract void CleanContainerCore( ListBoxContainer container );

    internal bool PrepareContainer( ListBoxContainer container, LayoutChangeInfo changeInfo )
    {
      if( this.PrepareContainerCore( container, changeInfo ) )
      {
        return true;
      }

      return false;
    }

    internal bool UpdateContainer( double normalizedElapsedTime, ListBoxContainer container )
    {
      bool retval = this.UpdateContainerCore( normalizedElapsedTime, container );

      container.UpdateLayoutTransform();
      container.UpdateSize();

      return retval;
    }

    private void CleanContainer( ListBoxContainer container )
    {
      //remove hooking done on container for this TransitionAnimation
      container.RemoveAnimation( typeof( PositionAnimation ), this );
      container.RemoveAnimation( typeof( SizeAnimation ), this );
      container.RemoveAnimation( typeof( OpacityAnimation ), this );
      container.RemoveAnimation( typeof( RotateAnimation ), this );
      container.RemoveAnimation( typeof( SizeAnimation ), this );
      container.RemoveAnimation( typeof( ScaleAnimation ), this );

      //Then call core method to allow customized cleaning of the Derived classes.
      this.CleanContainerCore( container );
    }

    private void OnRendering( object sender, EventArgs e )
    {
      //calculate normalized elapsed time 
      int elapsedTime = Environment.TickCount - this.StartTime;
      double normalizedElapsed = ( elapsedTime * 1.0 ) / this.Duration;

      normalizedElapsed = Math.Min( 1d, normalizedElapsed );

      //use array to avoid concurrency update issue.
      ListBoxContainer[] containers = new ListBoxContainer[ m_handledItems.Count ];
      m_handledItems.CopyTo( containers, 0 );

      foreach( ListBoxContainer container in containers )
      {

        if( this.UpdateContainer( normalizedElapsed, container ) )
        {
          this.CleanContainer( container );

          //remove
          m_handledItems.Remove( container );
        }
      }

      //Kill condition for Transition Animation. If it reaches or exceeds 1d normalized elapsed time, then stop everything.
      if( ( normalizedElapsed >= 1d ) && ( m_handledItems.Count > 0 ) )
      {
        //use array to avoid concurrency update issue.
        foreach( ListBoxContainer element in m_handledItems )
        {
          this.CleanContainer( element );
        }

        m_handledItems.Clear();
      }

      //If there are no more animated elements.
      if( m_handledItems.Count == 0 )
      {
        this.OnCompleted();
      }
    }

    private void OnCompleted()
    {
      if( this.IsRunning )
      {
        this.IsRunning = false;
        CompositionTarget.Rendering -= OnRendering;
      }

      this.HasCompleted = true;

      if( this.Completed != null )
      {
        this.Completed( this, new TransitionAnimationEventArgs( m_state ) );
      }

      m_initialized = false;
    }

    #region Private Members

    private object m_state = null;

    private bool m_initialized = false;
    private Dictionary<ListBoxContainer, ElementLayoutInfo> m_initialLayoutInformation;

    private readonly List<ListBoxContainer> m_handledItems = new List<ListBoxContainer>();



    #endregion

    #region Private ElementLayoutInfo Struct

    private struct ElementLayoutInfo
    {
      public double PositionX
      {
        get;
        set;
      }

      public double PositionY
      {
        get;
        set;
      }

      public double Width
      {
        get;
        set;
      }

      public double Height
      {
        get;
        set;
      }

      public double MaximumHeight
      {
        get;
        set;
      }

      public double MaximumWidth
      {
        get;
        set;
      }

      public double Opacity
      {
        get;
        set;
      }

      public double Angle
      {
        get;
        set;
      }

      public double ScaleFactor
      {
        get;
        set;
      }
    } 

    #endregion
   }
}
